在使用 Web Push 推送功能时,很多人会发现:调用
registration.pushManager.subscribe()
之后,浏览器会自动请求网站根目录下的 sw.js 文件,例如:
/sw.js
这并不是 pushManager.subscribe() 本身主动加载了 sw.js,而通常是因为 当前页面还没有注册并控制有效的 Service Worker,浏览器需要先完成 Service Worker 的注册、安装和激活流程,才能创建 Push Subscription。
本文记录这一过程,以及正确的使用方式。
一、什么是 pushManager.subscribe()
pushManager.subscribe() 用于创建一个 Push Subscription,也就是当前浏览器与推送服务之间的订阅关系。
基本代码如下:
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: publicVapidKey
});
console.log(subscription);
订阅成功后,会返回一个 PushSubscription 对象,通常包含:
endpointexpirationTimekeys.p256dhkeys.auth
这些信息需要发送到服务器保存,后续服务器才能根据该订阅向浏览器发送推送消息。
二、为什么会请求根目录下的 sw.js
关键代码通常是:
navigator.serviceWorker.register('/sw.js');
这段代码的作用是注册 Service Worker。浏览器会请求:
https://example.com/sw.js
其中 /sw.js 表示网站根目录下的 sw.js 文件。
完整流程通常如下:
await navigator.serviceWorker.register('/sw.js');
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: publicVapidKey
});
执行过程大致为:
浏览器请求
/sw.js下载并解析 Service Worker 文件
执行安装阶段
激活 Service Worker
等待 Service Worker 准备完成
调用
pushManager.subscribe()创建 Push Subscription
因此,看到浏览器访问 /sw.js 是正常现象。
三、Service Worker 为什么是 Web Push 的前提
Service Worker 是运行在后台的脚本,即使网页没有打开,它也可以接收推送消息。
例如,推送消息到达后,Service Worker 可以监听 push 事件:
self.addEventListener('push', event => {
const data = event.data
? event.data.json()
: {
title: '新消息',
body: '你收到了一条新消息'
};
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: '/icons/icon-192.png'
})
);
});
由于推送消息需要由 Service Worker 在后台接收和处理,所以浏览器必须先注册一个有效的 Service Worker。
四、推荐的注册和订阅代码
页面代码
async function subscribeToPush() {
if (!('serviceWorker' in navigator)) {
throw new Error('当前浏览器不支持 Service Worker');
}
if (!('PushManager' in window)) {
throw new Error('当前浏览器不支持 Web Push');
}
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
throw new Error('用户没有授予通知权限');
}
const registration = await navigator.serviceWorker.register('/sw.js');
await navigator.serviceWorker.ready;
const subscription =
await registration.pushManager.getSubscription();
if (subscription) {
return subscription;
}
const newSubscription =
await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(
'你的 VAPID 公钥'
)
});
return newSubscription;
}
辅助函数:
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
return Uint8Array.from(
[...rawData].map(char => char.charCodeAt(0))
);
}
调用:
subscribeToPush()
.then(subscription => {
console.log('Push Subscription:', subscription);
// 将 subscription 发送给服务器保存
})
.catch(error => {
console.error('订阅推送失败:', error);
});
五、为什么使用 /sw.js 而不是 ./sw.js
以下两种写法看起来相似:
navigator.serviceWorker.register('/sw.js');
navigator.serviceWorker.register('./sw.js');
但它们的作用域可能不同。
使用根路径
navigator.serviceWorker.register('/sw.js');
表示从网站根目录加载 Service Worker:
https://example.com/sw.js
默认作用域是整个站点:
https://example.com/
使用相对路径
如果当前页面地址是:
https://example.com/user/profile.html
执行:
navigator.serviceWorker.register('./sw.js');
浏览器可能会请求:
https://example.com/user/sw.js
默认作用域也可能被限制在 /user/ 目录下。
因此,如果希望 Service Worker 控制整个网站,通常使用:
navigator.serviceWorker.register('/sw.js');
六、sw.js 文件必须放在哪里
如果注册代码是:
navigator.serviceWorker.register('/sw.js');
服务器必须能够正确返回:
https://你的域名/sw.js
例如项目结构可以是:
public/
├── index.html
├── sw.js
└── icons/
└── icon-192.png
构建后,sw.js 应该被部署到网站根目录。
如果访问:
https://example.com/sw.js
返回的是 404、HTML 页面或错误内容,Service Worker 注册就会失败。
七、sw.js 的基本内容
一个最简单的 Service Worker 可以这样写:
self.addEventListener('install', event => {
console.log('Service Worker 安装中');
self.skipWaiting();
});
self.addEventListener('activate', event => {
console.log('Service Worker 已激活');
event.waitUntil(self.clients.claim());
});
self.addEventListener('push', event => {
const data = event.data
? event.data.json()
: {
title: '通知',
body: '你收到了一条推送消息'
};
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: '/icons/icon-192.png'
})
);
});
self.addEventListener('notificationclick', event => {
event.notification.close();
event.waitUntil(
clients.openWindow('/')
);
});
注意:sw.js 中不能直接使用页面脚本里的变量,也不能直接访问 window 和 document。
Service Worker 中使用的是:
self
而不是:
window
八、navigator.serviceWorker.ready 的作用
推荐使用:
const registration = await navigator.serviceWorker.ready;
navigator.serviceWorker.ready 会等待,直到当前页面对应的 Service Worker 注册完成并且处于激活状态。
例如:
const registration = await navigator.serviceWorker.ready;
await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: publicVapidKey
});
这样可以避免在 Service Worker 尚未激活时就调用订阅接口。
需要注意,ready 返回的是一个 ServiceWorkerRegistration 对象,而不是 Service Worker 脚本本身。
九、如果已经注册过,是否还会重复访问 sw.js
即使已经注册过 Service Worker,浏览器仍然可能再次检查或请求 sw.js,原因包括:
浏览器定期检查 Service Worker 是否有更新
页面重新打开
调用了
register(),浏览器需要确认脚本是否发生变化HTTP 缓存策略导致重新验证
Service Worker 版本发生更新
开发环境中 DevTools 设置了禁用缓存
这并不一定意味着 Service Worker 被重新安装了。
浏览器通常会先检查脚本内容是否变化,只有内容发生变化时,才会触发更新流程。
十、如何避免每次重复注册
可以将注册操作封装起来:
let registrationPromise;
function getServiceWorkerRegistration() {
if (!registrationPromise) {
registrationPromise =
navigator.serviceWorker.register('/sw.js');
}
return registrationPromise;
}
使用:
const registration = await getServiceWorkerRegistration();
const subscription =
await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: publicVapidKey
});
不过,即使多次执行 register('/sw.js'),浏览器也不会重复创建多个相同的 Service Worker 注册。浏览器会根据脚本地址和作用域管理已有注册。
十一、优先使用已有的订阅
在调用 subscribe() 之前,建议先调用:
const existingSubscription =
await registration.pushManager.getSubscription();
完整示例:
async function getPushSubscription() {
const registration =
await navigator.serviceWorker.ready;
let subscription =
await registration.pushManager.getSubscription();
if (!subscription) {
subscription =
await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: publicVapidKey
});
}
return subscription;
}
这样可以避免重复创建订阅。
十二、常见错误
1. Failed to register a ServiceWorker
可能原因:
/sw.js不存在返回内容不是 JavaScript
文件路径错误
Service Worker 脚本存在语法错误
当前网站不是 HTTPS
2. The operation is insecure
Service Worker 和 Push API 通常要求安全环境:
正式环境必须使用 HTTPS
本地开发时
localhost通常被浏览器视为安全来源
例如:
https://example.com
通常可以使用。
而:
http://example.com
通常不允许使用。
3. Registration failed - permission denied
可能原因:
用户拒绝了通知权限
浏览器或系统关闭了通知权限
当前页面不是安全来源
浏览器设置禁止通知
代码中应检查:
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
console.log('通知权限未获得');
}
4. No active Service Worker
说明 Service Worker 可能还没有激活。
可以使用:
const registration = await navigator.serviceWorker.ready;
不要只依赖:
navigator.serviceWorker.register('/sw.js');
因为 register() 返回时,Service Worker 不一定已经处于激活状态。
5. userVisibleOnly 错误
Chrome 等浏览器通常要求:
userVisibleOnly: true
这表示每一次推送消息都必须向用户展示可见通知。
正确写法:
registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: publicVapidKey
});
十三、如何排查 /sw.js 请求问题
可以打开浏览器开发者工具:
打开 DevTools
进入 Application
找到 Service Workers
查看当前注册状态
查看注册的脚本地址和作用域
点击 Unregister 取消注册
勾选 Update on reload 方便调试
同时在 Network 面板中检查:
/sw.js
重点查看:
HTTP 状态码是否为
200Content-Type是否正确返回内容是否真的是 Service Worker JavaScript
是否被重定向到登录页
是否被前端路由返回了
index.html是否存在缓存问题
十四、需要注意的关键点
1. Service Worker 文件必须使用正确的 MIME 类型
服务器最好返回:
Content-Type: application/javascript
或者:
Content-Type: text/javascript
不要把 sw.js 返回成 HTML。
2. Service Worker 作用域受脚本路径影响
例如:
navigator.serviceWorker.register('/assets/sw.js');
默认可能只能控制 /assets/ 目录。
如果希望控制根目录,需要:
navigator.serviceWorker.register('/sw.js');
或者使用服务器响应头扩大作用域:
Service-Worker-Allowed: /
但通常把 sw.js 放在网站根目录是最简单的方式。
3. Push 订阅不是永久不变的
Push Subscription 可能因为以下原因失效:
用户清除浏览器数据
用户取消网站权限
浏览器更换推送服务
订阅过期
浏览器或系统策略变化
因此服务器发送推送失败时,应考虑删除旧订阅,并允许客户端重新订阅。
十五、完整的推荐流程
async function initPush() {
if (!('serviceWorker' in navigator)) {
throw new Error('不支持 Service Worker');
}
if (!('PushManager' in window)) {
throw new Error('不支持 Push API');
}
const permission =
await Notification.requestPermission();
if (permission !== 'granted') {
throw new Error('通知权限未授权');
}
await navigator.serviceWorker.register('/sw.js');
const registration =
await navigator.serviceWorker.ready;
let subscription =
await registration.pushManager.getSubscription();
if (!subscription) {
subscription =
await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(
publicVapidKey
)
});
}
console.log('当前 Push Subscription:', subscription);
// 发送到服务器
await fetch('/api/push/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(subscription)
});
return subscription;
}
十六、结论
调用:
registration.pushManager.subscribe()
后访问根目录下的:
/sw.js
通常是正常现象。
根本原因是:
Web Push 必须依赖 Service Worker
Service Worker 通常通过
/sw.js注册浏览器需要确认 Service Worker 已安装并激活
浏览器可能定期重新检查
sw.js是否更新
推荐使用以下方式:
await navigator.serviceWorker.register('/sw.js');
const registration =
await navigator.serviceWorker.ready;
const subscription =
await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: publicVapidKey
});
只要 /sw.js 能够正常访问、页面使用 HTTPS、通知权限已授权,并且 Service Worker 已经成功激活,这种自动请求一般不需要额外处理。